/**
 * 
 */
package classifiers.perceptron;

import java.io.BufferedReader;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import classifiers.Classifier;
import classifiers.Results;

import preprocess.TextExtract;

import tools.Pair;
import tools.StringGetter;
import data.dictionary.Dictionary_Mapper;
import data.dictionary.Dictionary_Memory;
import data.dictionary.builder.DictionaryBuilderFromFile;
import data.dictionary.builder.DictionaryBuilderFromOfflineSet;
import data.dictionary.saver.DictionarySaver;
import data.factory.DocumentDirectoryOfflineSetFactory;
import data.factory.NumericalOfflineSetFromOctaveFile;
//import data.factory.DocumentDirectoryOfflineSetFactory.FilenameFilterDirectory;
import data.mapper.Mapper;
import data.output.OctaveSaver;
import data.preprocessing.StringProcessor;
import data.preprocessing.StringProcessor_FromStringProcessors;
import data.preprocessing.StringProcessor_LowerCase;
import data.preprocessing.StringProcessor_RemoveLineAndSpace;
import data.preprocessing.StringProcessor_RemovePunctuation;
import data.set.OfflineSet;
import data.set.OfflineSetIterator;
import data.set.adaptator.OfflineSet_Mapper;
import data.set.adaptator.TwoOfflineSet_To_PairOfflineSet;
import data.set.implementation.OfflineSet_Memory;
import data.vectors.DoubleVector;
import data.vectors.DoubleVectorIterator;
import data.vectors.DoubleVectorWritable;
import data.vectors.implementation.DoubleVector_Full_Memory;
import data.vectors.implementation.DoubleVector_Sparse_Memory;
import algorithm.classification.binary.PerceptronLoss_L2Regularized;
import algorithm.classification.binary.parameters.PerceptronParameters;
import algorithm.classification.evaluation.BinaryClassifierEvaluation;
import algorithm.classification.functionsandloss.HingeLoss;

/**
 * Classe wrappant l'ensemble des fonctions du perceptron normal.
 * @author simon
 *
 */
public class Perceptron implements Classifier {
	
	/**
	 * Le perceptron wrappé.
	 */
	private PerceptronLoss_L2Regularized percept ;
	
	/**
	 * Le dico utilisé par le perceptron
	 */
	private Dictionary_Memory<String> dico ;
	
	/**
	 * Le string_processor appliqué aux chaine (après l'extraction html) ;
	 */
	private StringProcessor sp ;
	
	/**
	 * Nom de la classe de score positif.
	 */
	private String posClass ;
	
	/**
	 * Nom de la classe de score negatif
	 */
	private String negClass ;
	
	/**
	 * Nom du perceptron ;
	 */
	private String name ;
	
	
	
	/**
	 * Le constructeur créé juste un perceptron avec des paramètres par défaut, et donne un nom au perceptron.
	 * La construction proprement dite est plutot faite par learn ou load.
	 */
	public Perceptron() {

		
		int nIterations = 300;
		int nbUpdate = 1000;
		double step = 1e-2;
		double lambda = 1e-4;
		boolean weightDecay = true;

		this.percept = 
			new PerceptronLoss_L2Regularized(
					new PerceptronParameters(nIterations, step, new HingeLoss(), lambda, nbUpdate, weightDecay));
		
		this.posClass="" ; this.negClass="" ;
		
	}
	
	/**
	 * Ce constructeur permet de créer un perceptron en initialisant les noms de classes et le nom du perceptron.
	 * Typiquement, il s'agira ensuite de lui faire apprendre ces classes.
	 */
	public Perceptron(String posClass, String negClass, String name) {

		int nIterations = 300;
		int nbUpdate = 1000;
		double step = 1e-2;
		double lambda = 1e-4;
		boolean weightDecay = true;

		this.percept = 
			new PerceptronLoss_L2Regularized(
					new PerceptronParameters(nIterations, step, new HingeLoss(), lambda, nbUpdate, weightDecay));
		
		this.negClass=negClass ; this.posClass=posClass ; this.name=name ;
		
	}
	
	
	/**
	 * Faire apprendre le perceptron
	 * Attention : Il faut lui donner le chemin d'un repertoire contenant des dossiers avec comme nom ceux des posClass et negClass.
	 * Attention : j'ai modifié le code, il prend des html qu'il transforme lui même.
	 * @throws IOException 
	 */
	public void learn(String path2data) throws IOException {
		
		
		String dicoFile = path2data+"dico.txt";
		String corpusNumFile = path2data+"corpusNum.txt";
		String labelNumFile = path2data+"labelNum.txt";
		
		/* Si les attribut neg/posClass n'ont pas été renseignés, on ne peut rien faire*/
		if(posClass=="" && negClass=="") {
			
			System.out.println("noms des classes non renseignés !") ;
			throw new IOException() ;
			
		}
		
		//================== 
		// chargement corpus par factory
		DocumentDirectoryOfflineSetFactory builder = new DocumentDirectoryOfflineSetFactory(path2data,new String[] {negClass , posClass});
		Pair< OfflineSet<String>, OfflineSet<Integer> > c = builder.build();
		OfflineSet<String> corpus = c.getX();
		OfflineSet<Integer> labels = c.getY();
		
		
		//===================
		// exemple d'utilisation:
		// definition d'iterateur sur les documents 
		OfflineSetIterator<String> iter = corpus.inputsetiterator();

		
		// Afficher le premier document:
		iter.next();
		System.out.println(iter.getCurrentObject());

		//====================
		// construction du dictionnaire: soit à partir du corpus, soit à partir d'un fichier

		boolean standardSP = true; 
		File fd = new File(dicoFile);
		Dictionary_Memory<String> dico;
		StringProcessor sp;
		if(standardSP)
			sp = Dictionary_Mapper.standardStringProcessor();
		else{
			StringProcessor_FromStringProcessors sp2 = new StringProcessor_FromStringProcessors();
			sp2.add(new StringProcessor_RemoveLineAndSpace());
			sp2.add(new StringProcessor_LowerCase());
			sp2.add(new StringProcessor_RemovePunctuation());
			sp2.add(new StringProcessor_RemoveLineAndSpace());
			sp = sp2;
		}
			

		if(!fd.isFile()){
			System.out.println("Construction du dictionnaire à partir du corpus");
			
			DictionaryBuilderFromOfflineSet dicoBuilder = new DictionaryBuilderFromOfflineSet(corpus, sp);

			dico = dicoBuilder.build();

			DictionarySaver<String> dicoSaver = new DictionarySaver<String>(dicoFile, dico);
			dicoSaver.save();
		}
		else{
			System.out.println("Chargement du dictionnaire à partir du fichier:" +dicoFile);
			
			// Alternative -> Construction à partir d'un fichier
			DictionaryBuilderFromFile dicLoader = 
				new DictionaryBuilderFromFile(dicoFile);
			dico = dicLoader.build();
		}

		//=====================
		// transformation des documents sous forme numeriques
		// On choisit de le faire une fois pour toute...

		boolean slowOption = false;
		fd = new File(corpusNumFile); File fd2 = new File(labelNumFile);
		OfflineSet<DoubleVector> corpusNumerique;
		Dictionary_Mapper mapper = new Dictionary_Mapper(dico , sp);

		if(slowOption){
			System.out.println("Corpus numérique obtenu par mapping online");
			corpusNumerique = new OfflineSet_Mapper<String,DoubleVector>(mapper, corpus);
		}
		else if(!fd.isFile() || !fd2.isFile()){
			System.out.println("Corpus numérique obtenu par mapping offline");
			
			OfflineSet_Memory<DoubleVector> corpTMP = new OfflineSet_Memory<DoubleVector>();
			

			iter = corpus.inputsetiterator();
			int cpt = 0;
			while(iter.hasNext()){
				iter.next();
				corpTMP.add(cpt, mapper.map((iter.getCurrentObject())));
				cpt++;
			}

			corpusNumerique = corpTMP;
			
			// sauvegarde
			OctaveSaver os = new OctaveSaver(corpusNumFile);
			os.saveOfflineSetDoubleVector(corpusNumerique, (int) dico.size());
			os = new OctaveSaver(labelNumFile);
			os.saveOSI(labels);
			
		}
		else{
			
			System.out.println("Corpus numérique obtenu par chargement de : "+corpusNumFile);
			
			NumericalOfflineSetFromOctaveFile ol = new NumericalOfflineSetFromOctaveFile(corpusNumFile, labelNumFile);
			Pair<OfflineSet<DoubleVector>, OfflineSet<Integer>> p = ol.build();
			corpusNumerique = p.getX();
			
		}

		// transformation des etiquettes en etiquettes numerique
		// On choisit de le faire à la volée... A partir d'un set + un mapper (cf ci-dessous)
		LabelMapper labelsMapper = new LabelMapper();

		OfflineSet_Mapper<Integer, Boolean> labelsNum = 
			new OfflineSet_Mapper<Integer, Boolean>(labelsMapper, labels);

		// reunificaiton des données et labels dans un set:

		OfflineSet<Pair<DoubleVector, Boolean> > dataLabeled = 
			new  TwoOfflineSet_To_PairOfflineSet<DoubleVector, Boolean>(corpusNumerique,labelsNum);

		//====================
		// Construction d'un classifieur
		int nIterations = 300;
		int nbUpdate = 1000;
		double step = 1e-2;
		double lambda = 1e-4;
		boolean weightDecay = true;

		this.percept = 
			new PerceptronLoss_L2Regularized(
					new PerceptronParameters(nIterations, step, new HingeLoss(), lambda, nbUpdate, weightDecay)); 

		System.out.println("Apprentissage d'un perceptron");
		this.percept.learn(dataLabeled);

		//====================
		// Evaluation des performances

		BinaryClassifierEvaluation<DoubleVector> eval = 
			new BinaryClassifierEvaluation<DoubleVector>(percept, dataLabeled);

		double[] perf = eval.evaluate();
		System.out.println("Confusion Matrix:");
		System.out.println(String.format("|%4f|%4f|", perf[0],perf[1] ));
		System.out.println(String.format("|%4f|%4f|", perf[2],perf[3] ));
		
		/*
		 *  J'insère ici la sauvegarde de dico et de sp en tant qu'attributs
		 */
		// TODO faire mieux de ça. C'est moche.
		this.dico=dico ;
		this.sp = sp ;
	}

	
	/**
	 * Sauvegarder le perceptron.
	 * Le fichier est créé s'il n'existe pas, et écrasé si il existe.
	 * @param path emplacement de la sauvegarde
	 * @throws IOException 
	 */
	public void save(String path) throws IOException {
		
		DoubleVector w = this.percept.getWeight() ;
		PrintWriter file =  new PrintWriter(new BufferedWriter(new FileWriter(path)));
		
		/* Tout d'abord, indiquer qu'il s'agit d'un perceptron */
		file.println("PERCEPTRON") ;
		
		/* Sauver le dico */
		DictionarySaver<String> dicoSaver = new DictionarySaver<String>(path+"_dico",this.dico);
		dicoSaver.save();
		
		// On va ensuite écrire le nom des classes et celui du perceptron.
		file.println(negClass) ;
		file.println(posClass) ;
		file.println(name) ;
		
		// Avant d'écrire le vecteurs des poids, on va écrire sa taille.
		file.println(w.size()) ;
		DoubleVectorIterator it=w.getIterator();
		while(it.hasNext())
		{
			it.next();
			double val=it.getCurrentValue();
			int fea=it.getCurrentFeature();
			if (val!=0)
			{
				file.print(fea);
				file.print(":");
				file.println(val);
			}
		}

		file.close() ;
		
	}
	
	
	/**
	 * Charge un perceptron sauvegardé.
	 * @param path emplacement de la sauvegarde
	 * @throws IOException 
	 */
	public void load(String path) throws IOException {
		
		BufferedReader  in = new BufferedReader(new FileReader(path));
		
		/* Verifier que le fichier chargé est bien un perceptron */
		if(!in.readLine().contains("PERCEPTRON")) {
			throw new IOException("Le fichier "+path+" n'est pas un perceptron.") ;
		}
		
		/* Charger les noms des classe et du perceptron */
		this.negClass=in.readLine() ;
		this.posClass=in.readLine() ;
		this.name=in.readLine() ;
		
		
		/* Puis creer un vecteur de la bonne taille pour les poids. */
		DoubleVectorWritable w = new DoubleVector_Sparse_Memory(Integer.parseInt(in.readLine())) ;
		
		String line;
		String[] temp;
		while ((line=in.readLine()) != null){  
		        temp=line.split(":");        
		        w.setValue(Integer.parseInt(temp[0]),Double.parseDouble(temp[1])) ;
		}  
		in.close();
		
		this.percept.setWeight(w) ;
		
		/* Charger le dico */
		DictionaryBuilderFromFile dicLoader = 
			new DictionaryBuilderFromFile(path+"_dico");
		this.dico = dicLoader.build();
		
	}
	
	
	/**
	 * Calcule l'étiquette associée à une chaine représentant un fichier HTML.
	 * @param docPath l'emplacement du fichier.
	 * @param dicoPath emplacement du dico.
	 * @throws IOException 
	 */
	public Results classifyString(String html) throws IOException {
		
		/* Extraire le texte seul */
		String content = TextExtract.extract(html) ;
		System.out.println(content);
		
		/* Creation d'une Map pour garder en mémoire la correspondance mot->stem. */
		Map<String,String> word2Stem = new HashMap<String,String>() ;
		
		/* Il faut ensuite projeter le document sous forme numérique grace au dico.*/
		Dictionary_Mapper dm = new Dictionary_Mapper(dico,Dictionary_Mapper.standardStringProcessor(word2Stem)) ;
		
		/* Nous pouvons enfin transformer le texte */
		DoubleVector docVector = dm.map(content) ;
		
		/* Reste plus qu'à appliquer le preceptron,et à construire un objet résultat */
		double score=this.percept.getScore(docVector) ;
		PerceptronResults r=new PerceptronResults(score > 0 ? posClass : negClass,score,word2Stem) ;
		r.computeColorsABS(dico,percept) ; r.computeColorsREL(dico,docVector,percept) ;
		
		return r ;
		
	}
	
	
	/**
	 * Calcule l'étiquette associée à un document html.
	 * @param docPath l'emplacement du fichier.
	 * @param dicoPath emplacement du dico.
	 * @throws IOException 
	 */
	public Results classifyFile(String docPath) throws IOException {
		
		return classifyString(StringGetter.getStringFromFile(docPath)) ;
		
	}
	
	/**
	 * Calcule l'étiquette associée à un document html situé à l'adresse donnée.
	 * @param docPath l'emplacement du fichier.
	 * @param dicoPath emplacement du dico.
	 * @throws IOException 
	 */
	public Results classifyPage(String url) throws IOException {
		
		return classifyString(StringGetter.getStringFromPage(url)) ;
		
	}
	
	
	
	/**
	 * Réaliser un test sur un ensemble de données étiquettés.
	 * @param path2data : emplacement des deux dossiers contenant les fichiers pour le test.
	 * @param dicoFile : emplacement du dico.
	 * @return matrice de confusion
	 * @throws IOException 
	 */
	public double[][] test(String path2data) throws IOException {		
		
		/* Chargement du corpus */
		DocumentDirectoryOfflineSetFactory builder = new DocumentDirectoryOfflineSetFactory(path2data,new String[] {negClass,posClass});
		Pair< OfflineSet<String>, OfflineSet<Integer> > c = builder.build();
		OfflineSet<String> corpus = c.getX();
		OfflineSet<Integer> labels = c.getY();

		//=====================
		// transformation des documents sous forme numeriques
		OfflineSet<DoubleVector> corpusNumerique;
		Dictionary_Mapper mapper = new Dictionary_Mapper(dico, Dictionary_Mapper.standardStringProcessor());
		corpusNumerique = new OfflineSet_Mapper<String,DoubleVector>(mapper, corpus);
		
		// transformation des etiquettes en etiquettes numerique
		// On choisit de le faire à la volée... A partir d'un set + un mapper (cf ci-dessous)
		LabelMapper labelsMapper = new LabelMapper();
		OfflineSet_Mapper<Integer, Boolean> labelsNum = 
			new OfflineSet_Mapper<Integer, Boolean>(labelsMapper, labels);

		// reunificaiton des données et labels dans un set:
		OfflineSet<Pair<DoubleVector, Boolean> > dataLabeled = 
			new  TwoOfflineSet_To_PairOfflineSet<DoubleVector, Boolean>(corpusNumerique,labelsNum);

		/* Evaluation des performances */
		BinaryClassifierEvaluation<DoubleVector> eval = 
			new BinaryClassifierEvaluation<DoubleVector>(this.percept, dataLabeled);
		double[] perf = eval.evaluate();
		return new double[][] {{perf[0],perf[1]},{perf[2],perf[3]}} ;
		
	}
	
	/**
	 * Retourner le nom.
	 */
	public String getName() {
		return name ;
	}


	public static void main (String[] args) throws IOException {
		
		/*System.out.println("Test") ;
		Perceptron p3 = new Perceptron() ;
		try {
			p3.learn("data/short/") ;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		p3.save("data/short/per") ;
		System.out.println(p3.classify("data/short/testA1","data/short/dico.txt")) ;
		System.out.println(p3.classify("data/short/testB1","data/short/dico.txt")) ; */
		
		/* *************************************************** */
		/* Bon gros test sur des données de wikipedia english. */
		/* *************************************************** */
		Perceptron pWiki = new Perceptron("sport","music","sportVSmusic") ;
		pWiki.learn("data/wiki1/learn/") ;
//		
		pWiki.save("data/wiki1/per") ;
//		
		pWiki.load("data/wiki1/per") ;
//		
//		double[][] r =pWiki.test("data/wiki1/test/") ;
//		System.out.print(r[0][0]+"|");
//		System.out.println(r[0][1]);
//		System.out.print(r[1][0]+"|");
//		System.out.println(r[1][1]);
		
		/* Creer 5 perceptron (test du buffer */
		Perceptron pWiki0 = new Perceptron("A","B","ab0") ;
		pWiki0.learn("data/short/") ;
		pWiki0.save("data/short/per0") ;
		Perceptron pWiki1 = new Perceptron("A","B","ab1") ;
		pWiki1.learn("data/short/") ;
		pWiki1.save("data/short/per1") ;
		Perceptron pWiki2 = new Perceptron("A","B","ab2") ;
		pWiki2.learn("data/short/") ;
		pWiki2.save("data/short/per2") ;
		Perceptron pWiki3 = new Perceptron("A","B","ab3") ;
		pWiki3.learn("data/short/") ;
		pWiki3.save("data/short/per3") ;
		Perceptron pWiki4 = new Perceptron("A","B","ab4") ;
		pWiki4.learn("data/short/") ;
		pWiki4.save("data/short/per4") ;
		
	}
	
	

	
}

class LabelMapper implements Mapper<Integer, Boolean>{

	public Boolean map(Integer from) {
		if(from.equals(1))
			return true;
		return false;
	}
}
